# Distributed under MIT License - See LICENSE
require 'family_tree_api/exceptions'
require 'family_tree_api/http_communicator'
require 'family_tree_api/url_encoder'
require 'family_tree_api/search/base'
require 'family_tree_api/person/base'
require 'family_tree_api/person/update_response'
require 'family_tree_api/values_person/base'
require 'family_tree_api/summary_person/base'
require 'family_tree_api/search/result'
require 'family_tree_api/match/base'
require 'rexml/document'
require "erb"


module FamilyTreeApi
  # FamilyTreeApi contains the method calls that map with the
  # FamilySearch API 'FamilyTree' web services
  class Client
    include ERB::Util
    attr_accessor :last_url # for debugging purposes
    
    def initialize(base_url, api_key, agent_string = 'Ruby-Fs-Api/.4')
      @base_url = base_url
      @api_key = api_key
      @agent_string = agent_string
    end
    
    # Prepares the instance with an http communicator 
    # and authenticated session
    def authenticate(username,password)
      begin
        @com = HttpCommunicator.new(:base_url => @base_url, :username => username, :password => password, :api_key => @api_key, :agent_string => @agent_string)
        true
      rescue Exception => e
        raise e
      end
    end
  
    # Sets the http communicator to use a previously established session
    def session=(sess)
      @com = HttpCommunicator.new(:base_url => @base_url, :session => sess, :agent_string => @agent_string)
    end
  
    # Returns the communicator's session id
    def session
      @com.session
    end
  
    # Makes the call to the Person Read Service.
    # ====Params:
    # * person_id: FamilyTree id of the person you wish to get information for
    #   this method only accepts one id
    # * options: accepts a Hash of of options
    #   * :ancestors - number 0 to 5 of levels of ancestors you wish to retrieve
    #   * :descendants - number 0 to 5 of levels of descendants you wish to retrieve
    def person(person_id, options = {})
      ancestors = options.delete(:ancestors) || 0
      descendants = options.delete(:descendants) || 0
      querystring = build_search_querystring options
      querystring = "&#{querystring}" if querystring != ''
      url = "/familytree/v1/person/#{person_id}?ancestors=#{ancestors}&descendants=#{descendants}#{querystring}"
      @last_url = url
      xml = get_response_body(url)
      person = FamilyTreeApi::Person::Base.parse xml, ancestors, descendants
    end
    
    # Makes the call to the Person Read Service with view=values (unique assertion values).
    # ====Params:
    # * person_id: FamilyTree id of the person you wish to get information for
    #   this method only accepts one id
    def values_person(person_id)
      url = "/familytree/v1/person/#{person_id}?view=values"
      @last_url = url
      xml = get_response_body(url)
      person = FamilyTreeApi::ValuesPerson::Base.parse xml
    end
    
    # Makes the call to the Person Read Service with a view=summary.
    #
    # This call returns a different type of person object because of the
    # different nature of the summary person xml.
    # ====Params:
    # * person_id: FamilyTree id of the person you wish to get information for
    #   this method only accepts one id
    # * options: accepts a Hash of of options
    #   * :ancestors - number 0 to 5 of levels of ancestors you wish to retrieve
    #   * :descendants - [not yet implemented] number 0 to 5 of levels of descendants you wish to retrieve
    def summary_person(person_id, options = {})
      ancestors = options.delete(:ancestors) || 0
      descendants = options.delete(:descendants) || 0
      options.delete(:view) # don't allow the view option
      querystring = build_search_querystring options
      querystring = "&#{querystring}" if querystring != ''
      url = "/familytree/v1/person/#{person_id}?view=summary&ancestors=#{ancestors}&descendants=#{descendants}#{querystring}"
      @last_url = url
      xml = get_response_body(url)
      person = FamilyTreeApi::SummaryPerson::Base.parse xml, ancestors, descendants
    end
  
    # Makes call to the Search Service.
    # ===Params:
    # * options: accepts a Hash of options. All options are optional but at least one given name, family name or name must be provided.
    #   * :maxResults - Max number of results to return.
    #   * :startIndex - zero-based index at which to start returning results used for paging.
    #   * :name - A full name (given and family name) of the primary person being searched for.
    #   * :givenName - The given name(s) of the individual person being searched for.
    #   * :familyName - Family name of person being searched for.
    #   * :gender - 'Male' or 'Female'
    #   * :birthDate - Date of birth, format "12 May 1862". You may submit
    #     just the data that you know (e.g.: year only, month and year, etc.)
    #   * :birthPlace - Birthplace of the primary person being searched for. Search 
    #     results could include persons with matching birth-like event places, such as christening.
    #   * :deathDate 
    #   * :marriageDate 
    #   * :marriagePlace
    #   * :father, :mother, :spouse - Hash containing any of the above options pertaining to the father, mother or spouse
    def search(options)
      querystring = build_search_querystring options
      url = "/familytree/v1/search?#{querystring}"
      @last_url = url
      xml = @com.get(url).body
      search_obj = FamilyTreeApi::Search::Base.parse(xml)
      search_obj.results
    end
    
    # Makes call to the Match Service.
    # ===Params:
    # * options: accepts a Hash of parameters, or a single string id like 'KWB5-PPQ'.
    #   The following are parameters you can set in the hash (all values should be strings):
    #   * :gender - 'Male' or 'Female' 
    #   * :givenName - The given name of the person being matched
    #   * :familyName - The surname or family name
    #   * :birthDate - The date of the person you are matching, can include the information
    #     that you know in the following format 12 November 1800. If you only know the year
    #     then just send 1800.
    #   * :birthPlace - Place of birth or christening
    #   * :deathDate - Date of death
    #   * :deathPlace - Place of death or burial
    #   * :marriageDate - Date of marriage or engagement
    #   * :marriagePlace - Place of marriage or engagement
    #   * :father - this should be associated with a hash with any of the above values.
    #     For example, you can send :father => {:givenName => 'Charles', :familyName => 'Zimmerman' }
    #   * :mother - same concept as :father
    #   * :spouse - same concept as :father
    #   * :child - same concept as :father
    # ===Please Note
    # If using a hash, :gender, :givenName, and :familyName are _required_, along with
    # any 2 of the following combinations:
    # * Birth event with a date and a place.
    # * Death event with a date and a place.
    # * Spouse with givenName and familyName.
    # * Father with givenName and familyName.
    # * Mother with givenName and familyName.
    # * Child with gender, givenName and familyName
    def match(options)
      raise "Invalid Parameters" unless options.is_a?(Hash) || options.is_a?(String)
      validate_match_hash(options) if options.is_a?(Hash)
      url = "/familytree/v1/match"
      url += (options.is_a? Hash) ? "?#{build_search_querystring(options)}" : "/#{options}"
      @last_url = url
      xml = get_response_body(url)
      results = FamilyTreeApi::Match::Matches.parse(xml)
    end
    
    # Combines two or more persons into a single person.
    # 
    # Returns the resulting id of the combined person.
    # ===Params
    # * ids: this is a list of person ids that you wish to combine. You must 
    #   provide at least two ids but may provide more.
    # 
    # ===Example
    #   client = FamilyTreeApi::Client.new 'http://www.dev.usys.org'
    #   client.authenticate 'username', 'password'
    #   client.combine 'KWQS-DK2', 'KNFW-4FD', 'BNRS-PT2' #=> 'KWTS-VDW'
    # 
    # ===Please Note
    # Special care should be given to ensure that the t
    def combine(*ids)
      #service requires the version numbers of each person to combine
      ref_version_hashes = ids.collect do |id|
        person = self.person id, :view => 'version'
        {:ref => id, :version => person.version }
      end
      xml = FamilyTreeApi::Person.combine_request(ref_version_hashes)
      res = @com.post('/familytree/v1/person', xml)
      result_xml = res.body
      doc = REXML::Document.new(result_xml)
      namespace = {'z' => 'http://api.familysearch.org/familytree/v1'}
      new_id = REXML::XPath.match(doc, "/z:familytree/z:persons/z:person[@tempId='JIMMYZ']/@id", namespace).first.to_s
      new_id
    end
    
    def save(person, assertions = :all )
      url = person.id.nil? ? '/familytree/v1/person' : "/familytree/v1/person/#{person.id}"
      res = @com.post(url, person.to_xml(assertions))
      update_response = FamilyTreeApi::Person::UpdateResponse.parse(res.body)
      person.id = update_response.id
      return update_response
    end
    
    private
    
    def validate_match_hash(options)
      raise "Insufficient Parameters" if options[:gender].nil? || options[:givenName].nil? || options[:familyName].nil?
    end
    
    def get_response_body(url)
      res = @com.get(url)
      res.body
    end
  
    def build_search_querystring(options)
      options.collect do |k,v|
        if v.class == Hash
          # if father, mother, or spouse hash is passed, flatten it to appropriate url string
          v.collect do |k2, v2|
            "#{k.to_s.url_encode}.#{k2.to_s.url_encode}=#{v2.to_s.url_encode}"
          end.join('&')
        else
          "#{k.to_s.url_encode}=#{v.to_s.url_encode}"
        end
      end.join('&')
    end
  end

end